查看原文
其他

.NET Core 3.0 进行跨平台 IoT 编程

DotNet 2021-09-23

(给DotNet加星标,提升.Net技能


转自:Dawid Borycki
msdn.microsoft.com/zh-cn/magazine/mt833493

Microsoft Build 2019 为 .NET 开发人员带来了令人激动的消息:.NET Core 3.0 现在支持 C# 8.0、Windows 桌面和 IoT,因此,可以使用现有的 .NET 技能为智能设备开发跨平台应用。


在本文中,将向你演示如何使用 Sense HAT 附加板为 Raspberry Pi 2/3 创建一个 .NET Core 应用。


该应用将获得各种传感器读数,并可通过 ASP.NET Core Web API 服务获取最新读数。我将使用 Swagger(图 1)为此服务创建简单的 UI,这样,你可以轻松地与 IoT 设备进行交互。


除了从设备获取数据外,还可以远程更改 Sense HAT LED 阵列的颜色(图 2)。可通过我的 GitHub 页面 bit.ly/2WCj0G2 获得随附的代码。



图 1 通过 Web API 从运行 .NET Core 3.0 应用的 IoT 设备获取传感器读数



图 2 IoT 设备的远程控制(带有 Sense HAT 附加板的 Raspberry Pi 2)


我的设备


首先,设置 IoT 设备,包括 Raspberry Pi 2(或简称 RPi2)和 Sense HAT 附加板(参阅图 2 右侧)。


RPi2 是一款流行的单板机,可以运行 Linux 或 Windows 10 IoT 核心版操作系统。例如,可以从 adafruit.com 获得该设备。Sense HAT 附加板配有多个传感器,包括温度计、气压计、磁力仪、陀螺仪和加速度计。


此外,Sense HAT 有 64 块 RGB LED,可以将其用作指示器或低分辨率屏幕。


Sense HAT 可以轻松连接到 RPi2,因此,可以快速获取传感器读数,而无需任何焊接。


为了启动 RPi2,我使用了 USB 硬件保护装置,然后使用 USB 适配器连接到本地 Wi-Fi 网络(因为 RPi2 与 RPi3 不同,它没有内置 Wi-Fi 模块)。


设置好硬件之后,我安装了 Windows 10 IoT 核心版。可以在 bit.ly/2Ic1Ew1 中找到完整说明。简单地说,你可将操作系统闪存到 microSD 卡上。这可以通过 Windows 10 核心板仪表板 (bit.ly/2VvXm76) 轻松实现。安装仪表板后,转到"设置新设备"选项卡,选择可用于 PC 的设备类型、操作系统内部版本、设备名称、管理员密码和 Wi-Fi 连接。接受软件许可条款并单击"下载并安装"。完成这一过程后,将 microSD 卡插入 IoT 设备并启动。你很快就会在仪表板的"我的设备"选项卡下看到该设备显示为一个条目。


公用库


在开始实际实现之前,我安装了 .Net Core 3.0 Preview 5。然后,我打开 Visual Studio 2019 并使用类库 (.NET Core) 模板创建了一个新项目。


我分别将项目和解决方案名称设置为 SenseHat.DotNetCore.Common 和 SenseHat.DotNetCore。


然后,我通过在包管理器控制台 (bit.ly/2KRvCHj) 中调用以下命令来安装 Iot.Device.Bindings NuGet 包 (github.com/dotnet/iot):


Install-Package IoT.Device.Bindings -PreRelease
-Source https://dotnetfeed.blob.core.windows.net/dotnet-iot/index.json


IoT.Device.Bindings 是一个适用于热门 IoT 硬件组件的开放源代码 .NET Core 实现,可让用户为 IoT 设备快速实现 .NET Core 应用。可在 src/devices/SenseHat 子文件夹下找到与此处关联度最高的 Sense HAT 绑定。快速浏览一下这段代码,就会发现 IoT.Device.Bindings 使用 I2C 总线来访问 Sense HAT 组件。


在展示如何使用 Sense HAT 的 IoT.Device.Bindings 之前,我首先实现了简单的 POCO 类 SensorReadings (SenseHat.DotNetCore.Common/Sensors/SensorReadings.cs):


public class SensorReadings
{
public Temperature Temperature { get; set; }
public Temperature Temperature2 { get; set; }
public float Humidity { get; set; }
public float Pressure { get; set; }
public DateTime TimeStamp { get; } = DateTime.UtcNow;
}


此类有五个公共成员。四个成员存储实际传感器读数、两个温度读数以及湿度和压力。存在两个温度,因为 Sense HAT 有两个温度传感器,一个嵌入在 LPS25H 压力传感器中 (bit.ly/2MiYZEI),另一个嵌入在 HTS221 湿度传感器中 (bit.ly/2HMP9GO)。温度由 Iot.Device.Bindings 中的 Iot.Units.Temperature 结构表示。


此结构没有任何公共构造函数,但可以使用以下静态方法之一进行实例化:FromCelsius、FromFahrenheit 或 FromKelvin。给定其中一个标度的温度,结构将一个值转换为其他单位。然后,可以通过读取相应的属性获得所选单位的温度:摄氏、华氏或开尔文。


SensorReadings 类的第五个成员 TimeStamp 包含记录传感器读数的时间点。在将数据流式传输到云,然后使用 Azure 流分析或时序见解等专用服务执行时间序列分析时,此功能非常有用。


此外,SensorReadings 类会覆盖 ToString 方法,以在控制台中正确显示传感器值:


public override string ToString()
{
return $"Temperature: {Temperature.Celsius,5:F2} °C"
+ $" Temperature2: {Temperature2.Celsius,5:F2} °C"
+ $" Humidity: {Humidity,4:F1} %"
+ $" Pressure: {Pressure,6:F1} hPa";
}


下一步是实现 SenseHatService 类来访问选定的 Sense HAT 组件。出于测试目的,我还决定实现另一个用作模拟器的 SenseHatEmulationService 类。我想快速测试 Web API 和代码的其他元素,而无需连接硬件。为了在两个具体实现之间轻松切换,我使用了依赖关系注入软件设计模式。为此,我首先声明了一个接口:


public interface ISenseHatService
{
public SensorReadings SensorReadings { get; }
public void Fill(Color color);
public bool EmulationMode { get; }
}


此接口定义了两个具体实现的公共成员:


  • SensorReadings:调用方可使用此属性获取从传感器中得到的值(如果使用 SenseHatService,则得到实际值),以及通过 SenseHatEmulationService 随机生成的值。


  • Fill:此方法将用于统一设置所有 LED 的颜色。


  • EmulationMode:此属性表示是否模拟 Sense HAT(true 或 false)。


接下来,我实现了 SenseHatService 类。


让我们先来看一下类构造函数,如图 3 所示。此构造函数实例化三个私有只读字段:ledMatrix、pressure­AndTemperatureSensor 和 temperatureAndHumiditySensor。为此,我使用了 IoT.Device.Bindings 包中的相应类:分别为 SenseHatLedMatrixI2c、SenseHatPressureAndTemperature 和 SenseHatTemperatureAndHumidity。每个类都有一个公共构造函数,它接受一个参数,即抽象类型 System.Device.I2c.I2cDevice 的 i2cDevice。


此参数的默认值为 null。在这种情况下,IoT.Device.Bindings 将在内部使用 System.Device.I2c.Drivers.UnixI2cDevice 或 System.Device.I2c.Drivers.Windows10I2cDevice 的实例,具体取决于操作系统。


图 3 SenseHatService 类的选定成员


public class SenseHatService : ISenseHatService
{
// Other members of the SenseHatService (refer to the companion code)
private readonly SenseHatLedMatrix ledMatrix;
private readonly SenseHatPressureAndTemperature pressureAndTemperatureSensor;
private readonly SenseHatTemperatureAndHumidity temperatureAndHumiditySensor;
public SenseHatService()
{
ledMatrix = new SenseHatLedMatrixI2c();
pressureAndTemperatureSensor = new SenseHatPressureAndTemperature();
temperatureAndHumiditySensor = new SenseHatTemperatureAndHumidity();
}
}

之后,可以获取传感器值(在随附的代码中,请参阅 SenseHat.DotNetCore.Common/Services/SenseHatService.cs):


public SensorReadings SensorReadings => GetReadings();
private SensorReadings GetReadings()
{
return new SensorReadings
{
Temperature = temperatureAndHumiditySensor.Temperature,
Humidity = temperatureAndHumiditySensor.Humidity,
Temperature2 = pressureAndTemperatureSensor.Temperature,
Pressure = pressureAndTemperatureSensor.Pressure
};
}

并设置 LED 阵列的颜色:


public void Fill(Color color) => ledMatrix.Fill(color);


不过,在尝试之前,让我们首先实现模拟器 SenseHatEmulationService。该类的完整代码可在随附代码 (SenseHat.DotNetCore.Common/Services/SenseHatEmulationService.cs) 中找到。该类派生自 ISenseHatService 接口,因此它必须实现前面描述的三个公共成员:SensorReadings、Fill 和 EmulationMode。


我首先开始合成传感器读数。为此,我实现了以下帮助程序方法:


private double GetRandomValue(SensorReadingRange sensorReadingRange)
{
var randomValueRescaled = randomNumberGenerator.NextDouble()
* sensorReadingRange.ValueRange();
return sensorReadingRange.Min + randomValueRescaled;
}
private readonly Random randomNumberGenerator = new Random();


GetRandomValue 使用 System.Random 类生成 double,其值在指定范围内。


此范围由 SensorReadingRange (SenseHat.DotNetCore.Common/Sensors/SensorReadingRange.cs) 的实例表示,该实例具有两个公共属性 Min 和 Max,它们指定模拟传感器读数的最小值和最大值。此外,SensorReadingRange 类实现一个实例方法 ValueRange,该方法返回 Max 和 Min 之间的差值。


GetRandomValue 用于 GetSensorReadings 私有方法,以合成传感器读数,如图 4 所示。


图 4 SenseHatEmulationService


private SensorReadings GetSensorReadings()
{
return new SensorReadings
{
Temperature = Temperature.FromCelsius(GetRandomValue(temperatureRange)),
Humidity = (float)GetRandomValue(humidityRange),
Temperature2 =Temperature.FromCelsius(GetRandomValue(temperatureRange)),
Pressure = (float)GetRandomValue(pressureRange)
};
}
private readonly SensorReadingRange temperatureRange = new SensorReadingRange { Min = 20, Max = 40 };
private readonly SensorReadingRange humidityRange = new SensorReadingRange { Min = 0, Max = 100 };
private readonly SensorReadingRange pressureRange = new SensorReadingRange { Min = 1000, Max = 1050 };


最后,我实现了公共成员:


public SensorReadings SensorReadings => GetSensorReadings();
public void Fill(Color color) {/* Intentionally do nothing*/}
public bool EmulationMode => true;


我还创建了 SenseHatServiceHelper 类,稍后我将使用该类 (SenseHat.DotNetCore.Common/Helpers/SenseHatServiceHelper.cs)。SenseHatServiceHelper 类有一个公共方法,该方法返回 SenseHatService 或 SenseHatEmulationService 的一个实例,具体取决于布尔参数 emulationMode:


public static ISenseHatService GetService(bool emulationMode = false)
{
if (emulationMode)
{
return new SenseHatEmulationService();
}
else
{
return new SenseHatService();
}
}


控制台应用


现在,我可以使用控制台应用测试代码。可在开发电脑或 IoT 设备上使用此应用。在电脑上运行时,应用可以使用模拟器。要在模拟和非模拟模式之间切换,我将使用一个命令行参数,它将是一个包含 Y 或 N 字母的字符串。控制台应用将解析这个参数,然后使用 SenseHatEmulationService (Y) 或 SenseHatService (N)。在模拟模式下,应用仅显示合成的传感器读数。在非模拟模式下,应用将显示从实际传感器获得的值,并且还将按顺序更改 LED 阵列颜色。


为了创建控制台应用,我使用一个使用控制台应用 (.NET Core) 项目模板创建的新项目 SenseHat.DotNetCore.ConsoleApp 补充了 SenseHat.DotNetCore 解决方案。然后,我引用了 SenseHat.DotNetCore.Common 并开始实现 Program 类。我定义了三个私有成员:


private static readonly Color[] ledColors =
{ Color.Red, Color.Blue, Color.Green };
private static readonly int msDelayTime = 1000;
private static int ledColorIndex = 0;


第一个成员 ledColors 是一组颜色,将按顺序使用以统一更改 LED 阵列。第二个成员 msDelayTime 指定访问连续传感器读数和更改 LED 阵列之间的持续时间。最后一个成员 ledColorIndex 存储 ledColors 集合中当前显示的颜色的值。


接下来,我编写了两个帮助程序方法:


private static bool ParseInputArgumentsToSetEmulationMode(string[] args)
{
return args.Length == 1 && string.Equals(
args[0], "Y", StringComparison.OrdinalIgnoreCase);
}

和:


private static void ChangeFillColor(ISenseHatService senseHatService)
{
if (!senseHatService.EmulationMode)
{
senseHatService.Fill(ledColors[ledColorIndex]);
ledColorIndex = ++ledColorIndex % ledColors.Length;
}
}


第一种方法 ParseInputArgumentsToSetEmulationMode 分析输入参数的集合(传递给 Main 方法)以确定是否应该使用模拟模式。仅当集合具有等于 y 或 Y 的一个元素时,该方法才返回 true。


第二种方法 ChangeFillColor 仅在模拟模式关闭时有效。如果是,该方法将从 ledColors 集合中获取一个颜色并将其传递给 SenseHatService 具体实现的 Fill 方法;然后递增 ledColorIndex。在此特定实现中,可以省略 ChangeFillColor 中的 if 语句,因为 SenseHatEmulationService 的 Fill 方法不执行任何操作。但是,通常应包含 if 语句。


最后,我实现了 Main 方法,如图 5 所示,该方法将所有内容连接在一起。首先,解析输入参数,并根据结果调用 SenseHatServiceHelper 的 GetService 静态方法。其次,我显示字符串以通知用户应用是否在模拟模式下工作。第三,我开始无限循环,可从中获取传感器读数,并最终更改 LED 阵列颜色。循环使用 msDelayTime 暂停应用执行。


图 5 控制台应用的入口点


static void Main(string[] args)
{
// Parse input arguments, and set emulation mode accordingly
var emulationMode = ParseInputArgumentsToSetEmulationMode(args);
// Instantiate service
var senseHatService = SenseHatServiceHelper.GetService(emulationMode);
// Display the mode
Console.WriteLine($"Emulation mode: {senseHatService.EmulationMode}");
// Infinite loop
while (true)
{
// Display sensor readings
Console.WriteLine(senseHatService.SensorReadings);
// Change the LED array color
ChangeFillColor(senseHatService);
// Delay
Task.Delay(msDelayTime).Wait();
}
}


将应用部署到设备


要在 RPi 中运行应用,可以将 .NET Core 3.0 SDK 下载到设备中,在设备中复制代码,生成应用,最后使用 dotnet 运行 .NET Core CLI 命令来执行应用。或者,可以使用开发电脑发布应用,然后将二进制文件复制到设备。在这里,我将选择第二个选项。


首先生成 SenseHat.DotNetCore 解决方案,然后在解决方案文件夹中调用以下命令:


dotnet publish -r win-arm


如果项目文件包含以下属性,则可以省略参数 -r win-arm:<RuntimeIdentifier>win-arm</Runtime­Identifier>。可以使用所选的命令行界面或 Visual Studio 中的包管理器控制台。如果使用的是 Visual Studio 2019,则还可以使用 UI 工具发布应用。为此,请转到“解决方案资源管理器”,右键单击 ConsoleApp 项目,然后从上下文菜单中选择“发布”。Visual Studio 将显示一个对话框,可以在其中选择“文件夹”作为发布目标。然后,在发布配置文件设置下,将“部署模式”设置为“自包含”,并将“目标运行时”设置为“win-arm”。


无论选择哪种方法,.NET Core SDK 都将准备二进制文件以进行部署。默认情况下,可以在 bin\$(Configuration)\netcoreapp3.0\win-arm\publish 输出文件夹中找到它们。将此文件夹复制到设备。复制这些文件最直接的方法是使用 Windows 文件资源管理器 (bit.ly/2WYtnrT)。打开文件资源管理器,在地址栏中输入设备的 IP 地址,然后加上双反斜杠,后跟 c$。我的 RPi 的 IP 为 192.168.0.109,因此,我键入了 \\192.168.0.109\c$。一段时间后,文件资源管理器将显示一个提示,询问你管理员的密码。请耐心等待,这可能需要几秒钟的时间。最后,将二进制文件复制到设备。


若要实际运行该应用,可以使用 PowerShell。最简单的方法是使用 IoT 仪表板,如图 6 所示。只需右键单击“我的设备”选项卡下的设备,然后选择“PowerShell”。出现提示时,需要再次键入管理员密码。然后转到包含二进制文件的文件夹,并通过调用以下内容执行应用:


.\SenseHat.DotNetCore.ConsoleApp N



图 6 使用 Windows 10 IoT 仪表板启动 PowerShell


你将看到实际传感器读数(如图 7 中所示),SenseHat LED 阵列将更改颜色。可以看到两个传感器报告的温度几乎相同。湿度和压力也在预期范围内。为了进一步确认一切正常,让我们引入一些更改。为此,请用手遮盖设备。如你所见,湿度将上升。在我的案例中,湿度从 37% 增加到 51%。



图 7 使用 Raspberry Pi 2 上执行的控制台应用获取传感器读数


Web API


使用 .NET Core,可以进一步执行操作,通过 Web API 服务公开传感器读数。我将使用 Swagger UI (bit.ly/2IEnXXV) 创建一个简单的 UI。借助此 UI,最终用户可向 IoT 设备发送 HTTP 请求,因为他会将这些请求发送到常规 Web 应用!我们来看一下这是如何工作的。


我首先通过另一个 ASP.NET Core Web 应用程序项目 SenseHat.Dot­NetCore.WebApp 扩展 SenseHat.DotNetCore 解决方案,使用 API 模板创建项目。然后,我引用了 SenseHat.DotNetCore.Common 项目并安装了 Swashbuckle NuGet 包 (Install-Package Swashbuckle.AspNetCore)。


有关在 ASP.NET Core Web 应用程序中设置 Swagger 的详细说明,请参阅 bit.ly/2BpFzWC,因此,我将省略所有详细信息,并仅显示在我的应用中设置 Swagger UI 所需的说明。所有这些更改都将在 SenseHat.DotNetCore.WebApp 项目的 Startup.cs 文件中实现。


首先,我实现了只读字段 openApiInfo:


private readonly OpenApiInfo openApiInfo = new OpenApiInfo
{
Title = "Sense HAT API",
Version = "v1"
};


然后,我通过添加以下语句修改了 ConfigureServices 方法:


services.AddSwaggerGen(o =>
{
o.SwaggerDoc(openApiInfo.Version, openApiInfo);
});


这些语句负责设置 Swagger 生成器。接下来,我为依赖关系注入注册了 ISenseHatService 接口的具体实现:


var emulationMode = string.Equals(Configuration["Emulation"], "Y",
StringComparison.OrdinalIgnoreCase);
var senseHatService = SenseHatServiceHelper.GetService(emulationMode);
services.AddSingleton(senseHatService);


此处,SenseHat 服务的具体实现是根据配置文件中的 Emulation 设置来设置的。模拟键在 appsettings.Development.json 中设置为 N,在 appsettings.json 中设置为 Y。因此,Web 应用将在开发环境中使用模拟器,在生产环境中使用真正的 Sense HAT 硬件。与任何其他 ASP.NET Core Web 应用一样,默认情况下为版本生成配置启用生产环境。


最后一个修改是通过设置 Swagger 终结点的说明来扩展 Configure 方法:


app.UseSwagger();
app.UseSwaggerUI(o =>
{
o.SwaggerEndpoint($"/swagger/
{openApiInfo.Version}/swagger.
json"
,
openApiInfo.Title);
});


然后,我实现了 Web API 控制器的实际类。在 Controllers 文件夹中,我创建了新文件 SenseHatController.cs,我对其进行了修改,如图 8 所示。SenseHatController 有一个公共构造函数,用于依赖关系注入以获取 ISenseHatService 的实例。对此实例的引用存储在 senseHatService 字段中。


图 8 SenseHatController Web API 服务的完整定义


[Route("api/[controller]")]
[ApiController]
public class SenseHatController : ControllerBase
{
private readonly ISenseHatService senseHatService;
public SenseHatController(ISenseHatService senseHatService)
{
this.senseHatService = senseHatService;
}
[HttpGet]
[ProducesResponseType(typeof(SensorReadings), (int)HttpStatusCode.OK)]
public ActionResult<SensorReadings> Get()
{
return senseHatService.SensorReadings;
}
[HttpPost]
[ProducesResponseType((int)HttpStatusCode.Accepted)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
[ProducesResponseType((int)HttpStatusCode.InternalServerError)]
public ActionResult SetColor(string colorName)
{
var color = Color.FromName(colorName);
if (color.IsKnownColor)
{
senseHatService.Fill(color);
return Accepted();
}
else
{
return BadRequest();
}
}
}


SenseHatController 有两个公共方法,Get 和 SetColor。第一个方法处理 HTTP GET 请求,并从 Sense HAT 附加板返回传感器读数。第二个方法 SetColor 处理 HTTP POST 请求。SetColor 有一个字符串参数 colorName。客户端应用使用此参数选择颜色,然后使用该颜色统一更改 LED 阵列颜色。


我现在可以测试该应用的最终版本。同样,我可以使用模拟器或真正的硬件来实现这一目的。让我们从开发电脑开始,使用 Debug 生成配置来运行应用。执行应用后,在浏览器地址栏中键入 localhost:5000/swagger。


请注意,如果使用随附的代码并使用 Kestrel Web 服务器执行应用,浏览器将自动打开,你将被重定向到 swagger 终结点。我使用 launchSettings.json 的 launchUrl 对其进行了配置。


在 Swagger UI 中,将看到一个包含 Sense HAT API 标头的页面。此标头正下方是带有 GET 和 POST 标签的两个行。如果单击其中一行,则会显示更详细的视图。


借助此视图,可以向 SenseHatController 发送请求,查看响应并查阅 API 文档(如前面的图 1 左侧所示)。若要显示传感器读数,请展开 GET 行,然后单击"试一试"和"执行"按钮。


将请求发送到 Web API 控制器并进行处理,传感器读数将显示在 Response 主体下(如前面的图 1 右侧所示)。


以类似的方式发送 POST 请求,只需在单击"执行"按钮之前设置 colorName 参数。


为了在设备上测试应用,我使用"发布"配置发布了应用,然后将生成的二进制文件部署到 Raspberry Pi(与使用控制台应用一样)。然后,我必须打开端口 5000(通过在 RPi2 上从 PowerShell 调用以下命令来完成该操作):


netsh advfirewall firewall add rule name="ASP.NET Core Web Server port"
dir=in action=allow protocol=TCP localport=5000


最后,我使用以下命令从 PowerShell 执行了应用:


.\SenseHat.DotNetCore.WebApp.exe --urls http://*:5000


附加的命令行参数 (urls) 用于将默认 Web 服务器终结点从 localhost 更改为本地 IP 地址 (bit.ly/2VEUIji),以便可以从网络中的其他设备访问 Web 服务器。


完成此操作后,我在开发电脑上打开浏览器,键入 192.168.0.109:5000/swagger,随即显示 Swagger UI(当然,你将需要使用设备的 IP)。然后,我开始发送 HTTP 请求以获取传感器读数并更改 LED 阵列颜色,如前面的图 1 和图 2 所示。


总结


在本文中,我演示了如何使用 .NET Core 3.0 实现跨平台的 IoT 应用。该应用在 Raspberry Pi 2/3 上运行,并与 Sense HAT 附加板的组件进行交互。


使用该应用,我通过 Iot.Device.Bindings 库获得了各种传感器读数(温度、湿度和大气压力)。后实现了 ASP.NET Core Web API 服务并使用 Swagger 创建了一个简单的 UI。现在只需单击几下鼠标,任何人都可以访问这些传感器读数并远程控制设备。

代码可以运行,而不会对其他系统进行任何更改,包括 Raspbian。


示例演示了 .NET 开发人员如何利用现有的技能和代码库来编程各种物联网设备。


推荐阅读

(点击标题可跳转阅读)

.NET Core跨平台UI框架CPF

.NET Core 3.0 多平台项目发布与部署

.NET Core AA.FrameWork应用框架介绍


看完本文有收获?请转发分享给更多人

关注「DotNet」加星标,提升.Net技能 

好文章,我在看❤️

: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存